隨著 Backstage 平台持續整合來自不同部門與領域的大量資料,我們可以透過搜尋引擎插件為使用者提供了快速查找資訊的便利。然而,伴隨資料量的爆炸性增長,如何從龐大的資料庫中篩選出最具價值的內容成為了新挑戰。另外使用者在面對繁雜資訊時,往往需要耗費大量時間整理與理解,嚴重影響工作效率。因此,優化搜尋結果的準確性與內容呈現方式,讓使用者能迅速鎖定核心資訊,成為提升使用者體驗的關鍵。
為解決這些問題,我們可以借助生成式 AI 的力量,進一步提升搜尋與內容摘要的效率與精準度。藉由 AI 的協助,我們能自動分類與篩選資料,並在搜尋過程中提供更具相關性的結果,或自動生成內容摘要,協助使用者快速掌握重點資訊,大幅節省檢閱大量資料的時間。
然而,基於企業資料安全性的考量,我們無法依賴外部第三方的 AI 服務,儘管這些服務在效能上更為可靠。為此,我們選擇在本地部署大型語言模型(LLM),以確保資料的私密性。這不僅保障了資料安全,還能根據企業需求靈活調整模型的運作方式,完美滿足內部的使用情境。
為了解決這個問題,「Local LLM 本地大型語言模型知識庫」的概念應運而生,通過本地部署的大型語言模型,將 Backstage 平台中的各種資料,轉化為利於語言模型分析的資訊,再應用大型模型來幫助使用者,更高效地從大量資料中找到所需內容與萃取精髓。
AnythingLLM 的定位:
AnythingLLM 是作為一個介面端口,負責與向量資料庫中的資料進行交互。查詢已經向量化並存儲在資料庫中的資料,並整合和管理不同的語言模型,讓各式模型能夠靈活搭配使用,根據使用者的需求生成對應的內容。並且擁有 Workspace 的概念,能夠切分不同工作區使用不同的模型與參數,並隔離使用者的對話內容或文件。
RAG 向量化資料處理與存儲:
Backstage 各類資料會先經過 RAG(Retrieval-Augmented Generation)處理,將資料轉換成向量格式,以便更高效地進行語意檢索提高相關性和精準度。轉換後的向量資料將存儲在像 LancerDB 這樣的向量資料庫中。
Ollama 模型運算:
當 AnythingLLM 從向量資料庫查詢到資料後,再透過 API 與大型語言模型運算平台 Ollama 連接, 負責進行進一步的運算,根據查詢結果進行內容的生成和摘要,並將相關資訊回傳給使用者。運算過程中使用顯示卡進行 GPU 加速,以提升效能。
還記得我們先前提到的 Mintlify 嗎?它巧妙地結合了技術文檔與 API 文檔,讓我們不再需要分別管理兩種不同結構的文件。透過 MDX 檔案格式,我們可以一併撰寫這兩種文檔,並自動生成美觀的文件風格。
Mintlify 的亮點不僅在於文件整合功能,更令人驚艷的是它的 AI 搜尋功能。這也成為了我們為 Backstage 打造 AI 助手的靈感來源。我們計劃開發與 Mintlify 類似的功能,不僅能快速搜尋文件中的資訊,還能統整概念並提供範例程式碼。更重要的是,我們將使用本地運算資源,確保企業資料的安全性。
將 AnythingLLM 整合到 Backstage 之中,很適合將 LLM 分眾提供給使用者使用,達到內部使用的 ChatGPT 效果,避免敏感資料的外洩。
Anything LLM 內建非常方便的 API 功能,我們可以針對每個不同的 Workspace,搭配不同的語言模型去調用,並且可以透過此 API 上傳檔案進行 RAG 向量化資料,為特定 Workspace 加入避免出現嚴重的幻覺問題,或者可以直接透過 Anything LLM 調整參數,切換為查詢模式。功能豐富並且易於調整與管理權限,是個非常有潛力的項目。
使用者可以直接在介面上傳文件進行向量化,也可以透過 API 的形式使用,透過 API 非常適合加入到 Backstage 的插件開發中使用。例如我們可以將專案中的相關程式碼都加入到獨立的 Workspace 中向量化,為每個專案打造專屬的 AI 助手,再根據使用者目前檢視的頁面進行判斷、切換 Workspace 來回應。
我們透過 Ollama 這個開源專案來執行 LLM,搭配 RTX 3090 顯示卡來運算開發,以測試用途來說這張顯卡綽綽有餘。啟用 Ollama 非常簡單,優點也是能夠一鍵啟動相關模型,我們利用它來測試多個知名 LLM,試圖找出比較適合我們的模型。另外一點是在 HuggingFace 這個平台上擁有更多 LLM 可供選擇,但是需要經過額外轉檔的設定,我們將不會在本篇討論到。
例如我們嘗試過體驗比較優秀的,國科會TAIDE計畫推出的模型,針對繁體中文與台灣相關的資訊特別精準,或是可以嘗試看看最近討論度很高的模型,LLM 自我檢查的特色,號稱效能超越 ChatGPT 4o。
taide/TAIDE-LX-7B-Chat · Hugging Face
圖片來源 - https://tenten.co/learning/reflection-70b/
包含 Ollama 配套使用的 UI 介面、啟用顯示卡運算的選項以及 Anything ,我們可以透過以下 docker-compose.yaml
快速啟動。
anything:
image: mintplexlabs/anythingllm:latest
container_name: anything
restart: always
ports:
- "3001:3001"
volumes:
- ./anythingllm/data:/app/server/storage
- ./anythingllm/env.txt:/app/server/.env
networks:
- app-network
ollama:
image: ollama/ollama:latest
container_name: ollama
ports:
- 11434:11434
volumes:
- .:/code
- ./ollama/ollama:/root/.ollama
#pull_policy: always
tty: true
restart: always
networks:
- app-network
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
volumes:
- ./ollama/open-webui:/app/backend/data
depends_on:
- ollama
ports:
- 8080:8080
environment:
- OLLAMA_BASE_URL=http://ollama:11434
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
networks:
- app-network
目前還在試作階段,我們先以直接調用 Ollama API ,並直接給予明確的資料來測試效果,我們可以接續前面 Day 17 : Backstage 插件開發 - 初探後端整合打造 LLM 聊天室
繼續將我們的後端串接上 Ollama API,為 Backstage 賦予最基本的 AI 助手功能。確認模式成熟與適合模型的選定後,可以再引入 AnythingLLM 進行細部客製化的功能整合,預計呈現的效果如下。
為了測試 LLM 的性能,我們選了一個已經在 Backstage 實現過的功能,並利用它來取得 Outlook 信件中的資料,我們可以先略過這方面的程式碼實現。假設可以透過此方法取得最新 5 封的郵件內文,並且先清除不重要的內容符號與格式等等,最後格式化輸出格式,方便 LLM 的解讀。
格式化信件的內容,使其更為整潔結構。
呼叫 Ollama 的 Prompt 詞設定與相關基礎參數呼叫。
當後端資料成功執行並產生結果後,前端的處理相對就簡單許多,類似之前在 StagerAi 上線狀態時的方式接著寫下去。製作這部分時是為了能快速展示 Demo,因此大量依賴 AI 協助撰寫,後續也沒有特別進行優化,整體程式碼可能稍顯凌亂,且仍在開發中。以下內容僅供參考。
首先定義一個與後端 API 獲取資料的相關處理方法,可以在插件中新增一個 api.tsx
檔案。
import { useApi, configApiRef } from '@backstage/core-plugin-api';
export const useApiService = () => {
const config = useApi(configApiRef);
const baseUrl = config.getString('backend.baseUrl');
// 呼叫後端檢查服務狀態
const checkApiStatus = async () => {
try {
const response = await fetch(`${baseUrl}/api/ollama-api/status`);
const data = await response.json();
if (response.ok && data && data.apiStatus === 'ok') {
return 'ok';
} else {
return 'error';
}
} catch (error) {
console.error('Error checking API status:', error);
return 'error';
}
};
// 呼叫後端取得信件摘要
const fetchEmailSummary = async () => {
try {
const response = await fetch(`${baseUrl}/api/ollama-api/email-summary`);
const data = await response.json();
if (response.ok && data && data.summary) {
return data.summary;
} else {
return '無法獲取郵件摘要,請稍後再試。';
}
} catch (error) {
console.error('Error fetching email summary:', error);
return '獲取郵件摘要時發生錯誤,請稍後再試。';
}
};
// 傳遞使用者輸入內容
const sendMessage = async (newMessage: string) => {
try {
const response = await fetch(`${baseUrl}/api/ollama-api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: newMessage }),
});
const data = await response.json();
if (response.ok && data && data.reply) {
return data.reply;
} else {
return '無法處理您的請求,請再試一次。';
}
} catch (error) {
console.error('Error sending message:', error);
return '發生錯誤,請稍後再試。';
}
};
return {
checkApiStatus,
fetchEmailSummary,
sendMessage,
};
};
繼續拓展基於 react-chat-widget
的聊天室互動前端功能。
import React, { useEffect, useState } from 'react';
import {
Widget,
addResponseMessage,
addUserMessage,
deleteMessages,
setQuickButtons,
} from 'react-chat-widget';
import { makeStyles } from '@material-ui/core/styles';
import { useApiService } from './api'; // 引入 API 模塊
import 'react-chat-widget/lib/styles.css';
const useStyles = makeStyles(() => ({
'@global': {
'.rcw-messages-container': {
padding: '8px',
},
'.rcw-conversation-container .rcw-header': {
backgroundColor: 'rgba(178, 34, 34)',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.4)',
backdropFilter: 'blur(10px)',
},
'.rcw-message .rcw-response': {
backgroundColor: '#FFDAB9',
color: '#000000',
borderRadius: '18px',
},
'.rcw-message .rcw-response .rcw-message-text': {
maxWidth: '500px',
},
'.rcw-message .rcw-client .rcw-message-text': {
backgroundColor: '#FFDAB9',
color: '#B22222',
borderRadius: '18px',
},
'.rcw-launcher': {
backgroundColor: '#B22222',
'&:hover': {
backgroundColor: '#CD5C5C',
},
},
'.rcw-close-button': {
background: 'none',
border: 'none',
padding: '8px',
cursor: 'pointer',
color: '#ffffff',
},
'.rcw-new-message': {
color: '#999999',
width: '100%',
cursor: 'not-allowed',
pointerEvents: 'none',
backgroundColor: '#f5f5f5',
},
'.rcw-picker-btn': {
display: 'none',
},
'.quick-button': {
border: '2px solid #FFDAB9',
borderRadius: '12px',
padding: '10px 20px',
background: 'linear-gradient(135deg, #FFDAB9, #FFA07A)',
color: '#B22222',
transition: 'background 0.3s, transform 0.3s',
'&:hover': {
backgroundColor: '#CD5C5C',
transform: 'scale(1.05)',
},
},
},
}));
export const StageraiChat = () => {
const classes = useStyles();
const [apiStatus, setApiStatus] = useState<'ok' | 'error' | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { checkApiStatus, sendMessage, fetchEmailSummary } = useApiService();
// 定時檢查
useEffect(() => {
const checkStatus = async () => {
const status = await checkApiStatus();
setApiStatus(status);
if (status === 'ok') {
addResponseMessage('嗨! 我隨時都在');
setQuickButtons([
{ label: '查看郵件摘要', value: 'email_summary' },
// { label: '其他幫助', value: 'other_help' },
]);
} else {
addResponseMessage('🔴 StagerAi 目前離線,請稍後再試。');
}
};
checkStatus();
const intervalId = setInterval(checkStatus, 300000);
return () => clearInterval(intervalId);
}, []);
// 模擬一個一個字串流輸出的效果
const typeMessage = (message, callback, onComplete) => {
let index = 0;
const length = message.length;
const interval = 10;
const type = () => {
if (index < length) {
if (index > 0) {
deleteMessages(1);
}
callback(message.substring(0, index + 1));
index++;
setTimeout(type, interval);
} else {
onComplete();
}
};
type();
};
// 快速按鈕事件處理
const handleQuickButtonClicked = (buttonValue: string) => {
if (isLoading) return;
if (buttonValue === 'email_summary') {
addUserMessage('嘿! Stager,摘要近5封郵件');
addResponseMessage('好的,處理中...');
handleFetchEmailSummary();
} else if (buttonValue === 'other_help') {
addResponseMessage('現在還沒辦法幫你。');
}
};
// react-chat-widget 相關功能
const handleNewUserMessage = async (newMessage: string) => {
setIsLoading(true);
const responseMessage = await sendMessage(newMessage);
typeMessage(
responseMessage,
(msg: string) => addResponseMessage(msg),
() => setIsLoading(false)
);
};
// 呼叫前端取得信件方法
const handleFetchEmailSummary = async () => {
setIsLoading(true);
const summary = await fetchEmailSummary();
typeMessage(
summary,
(msg: string) => addResponseMessage(msg),
() => setIsLoading(false)
);
};
// react-chat-widget 自定義外觀
const subtitle = apiStatus === 'ok' ? '🟢 在線中' : '🔴 離線';
return (
<div>
<Widget
handleNewUserMessage={handleNewUserMessage}
handleQuickButtonClicked={handleQuickButtonClicked}
title="StagerAI 幫手"
subtitle={subtitle}
profileAvatar="https://i.imgur.com/7UVRSVY.png"
launcherOpenLabel="開啟聊天窗口"
launcherCloseLabel="關閉聊天窗口"
senderPlaceHolder="目前尚未支援傳送訊息"
showCloseButton={true}
showTimeStamp={false}
emojis={true}
resizable={true}
/>
</div>
);
};
利用 Ollama 與 AnythingLLM 搭配打造本地化 AI 助手,目前是短期想測試的目標,由於在本地化大型語言模型的極限與適性,對我們來說還有許多未知處。在使用這些語言模型時,我們可以發現性能上其實與 ChatGPT 這類主流線上服務相差甚遠,當然使用的設備上也有一定的差距,我們計畫先行做出可用的應用場景,再慢慢提升效能的部分。本篇文章接續前面的 Backstage 插件開發 - 初探後端整合打造 LLM 聊天室
將後續的邏輯程式實現,在後續的開發方向,我們仍需要針對模型的 RAG、Fine-tuning 開始細部客製化模型的資料,並思考搭配 Anyting LLM 的使用場境。本篇文章提供了開發範例給讀者參考。
https://mintlify.com/
https://medium.com/@pang2258/anythingllm-ollama輕鬆架設多人用的客製化-rag-2d05954bf771
https://tenten.co/learning/reflection-70b/